#!/usr/bin/python
# -*- coding: utf-8 -*-

# Copyright (C) 1994  Ling Li
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Library General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.


import gobject

import ftplib, os, re, select, string, sys, time, logging, socket
from ftplib import FTP, error_temp, error_perm
from logging import debug, info, error, warning
from threading import Thread

import ftp_engine
from configuration import config_value
from ftp_exception import *
from utils import *

class AdvancedFTP(FTP):
    '''AdvancedFTP is an enhancement of ftplib of Python.'''
    
    timeout = None
    charset = 'UTF-8'

    def __init__(self, host='', user='', passwd='', acct=''):
        FTP.__init__(self, host, user, passwd, acct)

    def set_charset(self, charset):
        self.charset = charset

    def set_logger(self, logger):
        self.logger = logger

    def sanitize(self, s):
        if s[:5] == 'pass ' or s[:5] == 'PASS ':
            i = len(s)
            while i > 5 and s[i-1] in '\r\n':
                i = i-1
            s = s[:5] + '*'*(i-5) + s[i:]
        return s

    def getline(self):
        poll = select.poll()
        poll.register(select.POLLIN)
        poll.poll(1000)
        line = None
        while self.file != None and (line == None or line == ''):
            line = self.file.readline()
            if self.charset != None:
                if not isinstance(line, unicode):
                    try:
                        line = unicode(line, self.charset)
                    except UnicodeDecodeError:
                        pass
            if self.debugging > 1:
                self.logger.write('*get* %s\n' % line)
            if not line: raise EOFError
            if line[-2:] == ftplib.CRLF: line = line[:-2]
            elif line[-1:] in ftplib.CRLF: line = line[:-1]
        return line

    def putcmd(self, line):
        if self.debugging:
            self.logger.write('*cmd* %s\n' % self.sanitize(line))
        self.putline(line)

    def putline(self, line):
        if self.charset != None and isinstance(line, unicode):
            line = line.encode(self.charset) + ftplib.CRLF
        else:
            line = line + ftplib.CRLF
        if self.debugging > 1:
            if string.lowercase(line[:5]) == 'pass ':
                i = len(line)
                while i > 5 and line[i-1] in '\r\n':
                    i = i-1
                line = line[:5] + '*'*(i-5) + line[i:]
            self.logger.write('*put* %s\n' % line)
        self.sock.sendall(line)

    def getresp(self):
        resp = self.getmultiline()
        if self.debugging:
            self.logger.write('*resp* %s\n' % resp)
        self.lastresp = resp[:3]
        c = resp[:1]
        if c == '4':
            raise error_temp, resp
        if c == '5':
            raise error_perm, resp
        if c not in '123':
            raise error_proto, resp
        return resp

    def retrlines(self, cmd, callback = None):
        def to_unicode(line):
            if not isinstance(line, unicode):
                try:
                    line = unicode(line, self.charset)
                except UnicodeDecodeError:
                    pass
            callback(line)
            
        if self.charset != None and callback != None:
            FTP.retrlines(self, cmd, to_unicode)
        else:
            FTP.retrlines(self, cmd, callback)
        return
        
    def getwelcome(self):
        return self.welcome

    def connect(self, host = '', port = 0):
        if host: self.host = host
        if port: self.port = port
        msg = "getaddrinfo returns an empty list"
        for res in socket.getaddrinfo(self.host, self.port, 0, socket.SOCK_STREAM):
            af, socktype, proto, canonname, sa = res
            try:
                self.sock = socket.socket(af, socktype, proto)
                if self.timeout:
                    self.sock.settimeout(self.timeout)
                self.sock.connect(sa)
            except socket.error, msg:
                if self.sock:
                    self.sock.close()
                self.sock = None
                continue
            break
        if not self.sock:
            raise socket.error, msg
        self.af = af
        self.file = self.sock.makefile('rb')
        self.welcome = self.getresp()
        return self.welcome

    def abort(self):
        """The behaviou of abort method in Python's ftplib package is not
        correct. Due to RFC 959, before sending ABOR command, an IP and
        a Synch should send first, and ABOR should not send in urgent
        mode.

        Since usually ABOR and get/put command runned in different thread,
        but when ABOR command is sent, a get/put reply and a abort reply
        whould be returned in queue. So in order to made both of them
        recieve reply correctly, ABOR should wait a while before reading
        reply message, or all the reply message should read by one of them.
        """

        logging.debug('send IP')
        self.sock.sendall(chr(255) + chr(244))
        logging.debug('send Synch')
        self.sock.sendall(chr(255) + chr(242), ftplib.MSG_OOB)
         
        line = 'ABOR'
        if self.debugging > 1: print '*put urgent*', self.sanitize(line)
        print '*cmd* ABOR'
        logging.debug('send ABOR')
        self.putline(line)
        resp = None
        while resp == None or resp[:3] != '226':
            resp = self.getmultiline()
            print resp
        return

    def on_timeout_change(self, config, id, value, *args):
        self.timeout = value.get_int()
        self.sock.settimeout(self.timeout)
        return

class FTPLibClient:
    """FTPLibClient is an implementation of FTPClient using ftplib package
    of Python."""

    class DebugLogger:
        """This class is used to write debug messages to log view. It has
        a write function which can be used to write messages."""

        charset = 'utf-8'
        
        def __init__(self, client, updater):
            self.client = client
            self.updater = updater
            self.line = ''

        def write(self, s):
            if not isinstance(s, unicode):
                try:
                    s = unicode(s, self.charset)
                except UnicodeDecodeError:
                    pass
            gobject.idle_add(self.updater, s)

        def flush(self):
            gobject.idle_add(self.updater, self.line)

        def set_charset(self, charset):
            self.charset = charset

    poll = None
    monitor_thread = None

    def __init__(self, ftp_engine):
        self.ftp_engine = ftp_engine
        self.config = ftp_engine.config
        self.coralftp = ftp_engine.coralftp
        self.sout = sys.stdout
        self.logger = FTPLibClient.DebugLogger(self, ftp_engine.log_updater)
        return

    def __del__(self):
        if self.input_hid:
            self.stop_monitor()
        return

    def run(self):
        "Start FTPClient."
        self.ftp = AdvancedFTP()
        self.ftp.set_logger(self.logger)
        self.ftp.set_debuglevel(1)
        self.ftp.timeout = config_value(
            self.config, 'general', 'connect_timeout')
        self.config.notify_add('general', 'connect_timeout',
                               self.ftp.on_timeout_change)
        return

    def interrupt(self):
        "Send an abort messasge"
        
        sys.stdout = self.logger
        self.logger.write('*error* (soft abort)\n')
        try:
            self.ftp.abort()
        except socket.timeout:
            logging.debug('timeout')
            pass
        self.logger.write('*error* User Aborted\n')
        sys.stdout = self.sout
        return

    def quit(self):
        QuitCommand(self).execute()
        self.monitor_thread.quit = True
        return

    def start_monitor(self):
        """Monitor is used when nothing is being done. This is used to get
        421 idle connection close inform."""

        if self.poll == None:
            self.poll = select.poll()
            self.monitor_thread = MonitorThread(self)
            self.monitor_thread.start()
        self.poll.register(self.ftp.file.fileno(), select.POLLIN)
        logging.debug('poll for ftp registered')
        return

    def stop_monitor(self):
        if self.poll:
            self.poll.unregister(self.ftp.file.fileno())
            logging.debug('poll for ftp unregistered')
        return
    
    def new_command(self, cmd_type, *args):
        if cmd_type == ftp_engine.CMD_QUIT:
            return QuitCommand(self, *args)
        elif cmd_type == ftp_engine.CMD_OPEN:
            return OpenCommand(self, *args)
        elif cmd_type == ftp_engine.CMD_CD:
            return CdCommand(self, *args)
        elif cmd_type == ftp_engine.CMD_LIST:
            return ListCommand(self, *args)
        elif cmd_type == ftp_engine.CMD_LISTD:
            return ListdCommand(self, *args)
        elif cmd_type == ftp_engine.CMD_PWD:
            return PwdCommand(self, *args)
        elif cmd_type == ftp_engine.CMD_PUT:
            return PutCommand(self, *args)
        elif cmd_type == ftp_engine.CMD_GET:
            return GetCommand(self, *args)
        elif cmd_type == ftp_engine.CMD_MKDIR:
            return MkdirCommand(self, *args)
        elif cmd_type == ftp_engine.CMD_DELETE:
            return DeleteCommand(self, *args)
        elif cmd_type == ftp_engine.CMD_MOVE:
            return MoveCommand(self, *args)
        elif cmd_type == ftp_engine.CMD_ABOR:
            return AborCommand(self, *args)
        elif cmd_type == ftp_engine.CMD_SIMPLE:
            return SimpleCommand(self, *args)
        else:
            raise ValueError

    def execute_command(self, command):
        return command.execute()

def safe_execute(logger, func, *args):
    sout = sys.stdout
    sys.stdout = logger
    try:
        try:
            return func(*args)
        finally:
            sys.stdout = sout
    except EOFError:
        # EOFError is accured when abort transfer
        logging.debug("EOFError")
    except error_temp, resp:
        logging.debug("TempError: %s" % resp.__str__())
        raise FTPTempException(resp)
    except error_perm, resp:
        logging.debug("PermError: %s" % resp.__str__())
        raise FTPPermException(resp)
    except ftplib.Error, resp:
        logging.debug(resp.__str__())
        raise FTPException('000 ' + resp.__str__())
    except socket.error, resp:
        logging.debug(resp.__str__())
        if isinstance(resp, list):
            raise NetworkException(resp)
        else:
            raise NetworkException((0, resp.__str__()))
    
class OpenCommand:

    def __init__(self, client, site_info):
        self.client = client
        self.ftp = client.ftp
        self.site_info = site_info
        if site_info['remote_charset']:
            self.ftp.set_charset(site_info['remote_charset'])
            self.client.logger.set_charset(site_info['remote_charset'])
        if not site_info['use_pasv'] in (True, 'True'):
            print site_info['use_pasv']
            self.ftp.set_pasv(False)

    def execute(self):
        # exception should be processed
        safe_execute(self.client.logger, self.ftp.connect,
                     self.site_info['server_addr'],
                     int(self.site_info['server_port']))

        sys.stdout = self.client.logger
        welcome = self.ftp.getwelcome()
        sys.stdout = self.client.sout
        
        if welcome and welcome != '' and len(welcome.split('\n')) > 2:
            gobject.idle_add(self.client.ftp_engine.emit,
                             'welcome-message', welcome)
                                 
        username = self.site_info['username']
        password = self.site_info['password']
        if username == 'None' and (password == '' or password == None):
            password ='unknown@coralstudio.org'

        safe_execute(self.client.logger, self.ftp.login, username, password)
        
class CdCommand:

    def __init__(self, client, path):
        if path == '':
            path = '/'
        self.client = client
        self.ftp = client.ftp
        self.path = path

    def execute(self):
        safe_execute(self.client.logger, self.ftp.cwd, self.path)

class ListCommand:

    def __init__(self, client, updater=None, dir='', clear_cache=0):
        self.client = client
        self.ftp = client.ftp
        self.updater = updater
        self.dir = dir
        self.clear_cache = clear_cache

    def execute(self):
        self.filelist = []
        self.rec = re.compile('([drwxsS\-]{10,11}) +(\d+) +(\w+) +(\w+) +(\d+) +(\w+) +(\d+) +([\w\:]+) +(.*)')
        safe_execute(self.client.logger, self.ftp.retrlines,
                     "LIST -a", self.update)
        if len(self.filelist) > 0 and self.updater:
            self.updater(self.filelist)

    def update(self, line):
        m = self.rec.match(line)
        if m != None:
            attr = m.group(1)
            childnum = m.group(2)
            owner = m.group(3)
            group = m.group(4)
            size = m.group(5)
            month = m.group(6)
            day = m.group(7)
            year_or_time = m.group(8)
            name = m.group(9)
            self.filelist.append((attr, childnum,
                            owner, group, size, month, day,
                            year_or_time, name))
            if len(self.filelist) == 10:
                if self.updater:
                    self.updater(self.filelist)
                    self.filelist = []
        
class ListdCommand(ListCommand):

    def __init__(self, client, dir):
        ListCommand.__init__(self, client, dir=dir, clear_cache=1)

    def execute(self):
        self.filelist = []
        self.rec = re.compile('([drwxsS\-]{10,11}) +(\d+) +(\w+) +(\w+) +(\d+) +(\w+) +(\d+) +([\w\:]+) +(.*)')
        safe_execute(self.client.logger, self.ftp.retrlines,
                     "LIST -d %s" % self.dir, self.update)
        return len(self.filelist) > 0

class QuitCommand:

    def __init__(self, client):
        self.client = client
        self.ftp = client.ftp

    def execute(self):
        safe_execute(self.client.logger, self.ftp.quit)
        safe_execute(self.client.logger, self.ftp.close)
        
class PwdCommand:

    def __init__(self, client, updater):
        self.client = client
        self.ftp = client.ftp
        self.updater = updater

    def execute(self):
        r = safe_execute(self.client.logger, self.ftp.pwd)
        gobject.idle_add(self.updater, r)

class PutCommand:

    def __init__(self, client, source, target, mode='b', rupdater=None):
        self.client = client
        self.ftp = client.ftp
        self.source = source
        self.target = target
        self.mode = mode
        self.rupdater = rupdater

    def execute(self):
        if self.mode == 'b':
            safe_execute(self.client.logger, self.ftp.voidcmd, 'TYPE I')
        else:
            safe_execute(self.client.logger, self.ftp.voidcmd, 'TYPE A')
        size = 0
        # if self.resume:
        #     size = os.stat(self.source)
        fp = open(self.source)
        # if size: fp.seek(size)
        cmd = 'STOR %s' % self.target
        try:
            conn = None
            conn = safe_execute(self.client.logger, self.ftp.transfercmd,
                                cmd, size)
            
            t1 = time.time()
            while 1:
                buf = fp.read(8192)
                if not buf: break
                try:
                    conn.sendall(buf)
                except socket.error, resp:
                    # if data socket error, it may be caused by abort command
                    if resp[0] == 32:
                        break
                    else:
                        raise NetworkException(resp)
                size = size + len(buf)
                t2 = time.time()
                if t2 - t1 > 1:
                    gobject.idle_add(self.rupdater, size, '')
                    t1 = time.time()
        finally:
            logging.debug('Close data connection')
            if conn:
                conn.close()
            fp.close()
        
        # if ABOR command is sent, leave all the message to abort method
        if self.client.ftp_engine.status == ftp_engine.STATUS_ABORT:
            return
        
        safe_execute(self.client.logger, self.ftp.voidresp)
    
class GetCommand:

    def __init__(self, client, source, target, mode='b', resume=0, rupdater=None, fupdater=None):
        self.client = client
        self.ftp = client.ftp
        self.source = source
        self.target = target
        self.mode = mode
        self.resume = resume
        self.rupdater = rupdater
        self.fupdater = fupdater

    def execute(self):
        if self.mode == 'b':
            safe_execute(self.client.logger, self.ftp.voidcmd, 'TYPE I')
            fm = 'wb'
        else:
            safe_execute(self.client.logger, self.ftp.voidcmd, 'TYPE A')
            fm = 'w'
        size = 0
        if self.resume:
            size = os.stat(self.target).st_size
            if fm == 'wb': fm = 'ab'
            else: fm = 'a'
            self.rupdater.start_bytes = size
        fp = open(self.target, fm)
        cmd = "RETR %s" % self.source
        try:
            conn = None
            conn = safe_execute(self.client.logger,
                                self.ftp.transfercmd,
                                cmd, size)
            
            t1 = time.time()
            while 1:
                try:
                    buf = conn.recv(1024)
                except socket.error, resp:
                    if resp[0] == 32:
                        break
                    else:
                        raise NetworkException(resp)
                if self.client.ftp_engine.status \
                       != ftp_engine.STATUS_RUNNING:
                    break
                if not buf:
                    break
                fp.write(buf)
                size = size + len(buf)
                t2 = time.time()
                if t2 - t1 > 1:
                    gobject.idle_add(self.rupdater, size, '')
                    t1 = time.time()

        finally:
            if conn:
                logging.debug('Close data connection')
                conn.close()
            fp.close()

        # if ABOR command is sent, leave all the message to abort method
        if self.client.ftp_engine.status == ftp_engine.STATUS_ABORT:
            return
        
        safe_execute(self.client.logger, self.ftp.voidresp)
        return
    
class DeleteCommand:

    def __init__(self, client, name, isdir):
        self.client = client
        self.ftp = client.ftp
        self.name = name
        self.isdir = isdir

    def execute(self):
        if self.isdir:
            safe_execute(self.client.logger, self.ftp.rmd, self.name)
        else:
            safe_execute(self.client.logger, self.ftp.delete, self.name)

class MoveCommand:

    def __init__(self, client, old_name, new_name):
        self.client = client
        self.ftp = client.ftp
        self.old_name = old_name
        self.new_name = new_name

    def execute(self):
        safe_execute(self.client.logger, self.ftp.rename,
                     self.old_name, self.new_name)

class MkdirCommand:

    def __init__(self, client, name):
        self.client = client
        self.ftp = client.ftp
        self.name = name

    def execute(self):
        safe_execute(self.client.logger, self.ftp.mkd, self.name)

class AborCommand:

    def __init__(self, client):
        self.client = client
        self.ftp = client.ftp

    def execute(self):
        safe_execute(self.client.logger, self.ftp.abort)

class SimpleCommand:

    def __init__(self, client, cmd):
        self.client = client
        self.ftp = client.ftp
        self.cmd = cmd

    def execute(self):
        safe_execute(self.client.logger, self.ftp.voidcmd, self.cmd)

class MonitorThread(Thread):

    quit = False

    def __init__(self, ftp_client):
        Thread.__init__(self)
        self.ftp_client = ftp_client
        self.ftp_engine = ftp_client.ftp_engine
        self.poll = ftp_client.poll

    def run(self):
        logging.debug('Monitor thread start')
        while True:
            l = self.poll.poll(1000)
            if self.quit: break
            if self.ftp_engine.status != ftp_engine.STATUS_IDLE: continue
            if not self.quit and len(l) > 0:
                try:
                    sys.stdout = self.ftp_client.logger
                    self.ftp_client.ftp.getresp()
                    sys.stdout = self.ftp_client.sout
                except EOFError:
                    logging.debug("EOFError")
                    break
                except ftplib.Error, resp:
                    resp = resp.__str__()
                    if resp[0:3] == '421':
                        gobject.idle_add(self.ftp_engine.emit,
                                        'idle-connection-closed')
                    break
                except socket.error, resp:
                    break
        logging.debug('Monitor thread quit')
        return
